// uncompresshelp.m
// cc uncompresshelp.m -g -O -s -o uncompresshelp -lIndexing_s
//
// Copyright 1993, 1994, Scott Hess.  This source code may be
// redistributed and modified without restriction.  Well, one
// restriction - do not claim that you wrote it.  If the user
// receives non-trivial advantage from the use of this program, a
// payment of $50-$100 would be appreciated.
//
// [I guess that would make this semi-shareware.  I'm not much
//  interested in turning a profit (Ha!) on this program.  Rather,
//  I am interested in getting some small return on time and energy
//  invested so that I can work on other, similar programs.  You
//  know, little programs that aren't worth doing for their own
//  sake, but when you need them, you NEED them.  I love poking
//  around in the obscure corners of NeXTSTEP, but too often I
//  can't spend much time doing it because it's hard to justify
//  the hours.  It's like a hobby that brings in a small income.
//  You wouldn't want to live on it, because in reality the income
//  barely covers equipment and supplies, but it's useful as
//  justification when someone says "Why do you keep wasting your
//  time with that?"  I also want to know if people find stuff like
//  this _useful_, or just interesting ...]
//
// Scott Hess
// 12901 Upton Avenue South, #326
// Burnsville, MN  55337
//
//
// uncompresshelp [helpfile.store [outputdir]]
//
// uncompresshelp extracts help files from a help storefile and
// stores them back onto the filesystem.  helpfile.store is a help
// storefile as generated by compresshelp.  outputdir is the
// directory to store the files in.  helpfile.store defaults to
// Help.store.  outputdir defaults to the input file with .store
// stripped from the end.
//
//
// To muck about with the Help.store files:
//  o  Open an IXStoreFile instance on the file.
//  o  Within the store, open an IXBTree on block one, which contains
//     entries mapping the name of the helpfile element to the
//     files which are contained within it.
//  o  Within the store, open an IXBTree on block two, which contains
//     entries mapping integer indices to file contents.
//  o  The directory btree's values are of the form
//     "filename key;[filename key;...]".  The filenames are files
//     that are contained within the helpfile element, while the
//     keys are integers which index into the file btree's to find
//     the contents of the file.
//  o  Individual file content keys may appear more than once in
//     the directory.  This means that the specified files are the
//     same [implemented as a call to link() in extractFile].
//  o  File's which end in .rtf are compressed using /usr/ucb/compress.
//     So is .index.store, apparently.
//
//
// Version history:
//
// Wednesday, January 12, 1994:
//     Fix inverted cur test in extractFile().
//
//     Rewrite uncompress call in terms of raw fork/execl/wait4.
//     Previously it used system(), which did not work correctly
//     when the filename contained spaces.  It also wouldn't have
//     worked if the filename contained shell metacharacters (which
//     was the reasoning behind not just putting quotes or
//     double-quotes in the system call).
//
//     Added more descriptive and debugging output.
//
// Monday, January 10, 1994:
//     First public release.
#import <libc.h>
#import <indexing/indexing.h>
#import <objc/HashTable.h>

static HashTable *extractedFiles=nil;
int uncompress( const char *filename)
{
    int pid;
    if( !(pid=fork())) {
	execl( "/usr/ucb/uncompress", "/usr/ucb/uncompress", filename, NULL);
	exit( 1);
    } else if( pid==-1) {
	perror( "uncompresshelp: uncompress error");
	return -1;
    } else {
	union wait status;
	int ret=wait4( pid, &status, 0, NULL);
	if( ret==-1) {
	    perror( "uncompresshelp: waiting for uncompress");
	    return -1;
	} else if( WIFSIGNALED( status)) {
	    fprintf( stderr, "uncompresshelp: compress terminated on signal %d.\n", status.w_termsig);
	    return -1;
	} else if( status.w_retcode) {
	    fprintf( stderr, "uncompresshelp: compress exited with %d.\n", status.w_retcode);
	    return -1;
	}
    }
    return 0;
}
BOOL extractFile( IXBTreeCursor *fileCursor, unsigned fileId, const char *target)
{
	// If we've already extracted that index, just link the
	// target to it.
    const char *cur=[extractedFiles valueForKey:(void *)fileId];
    if( cur) {
	fprintf( stderr, "\tLinking to %s\n", cur);
	if( link( cur, target)==-1) {
	    perror( "uncompresshelp: linking to extracted file");
	}
	
	// Otherwise, we have to reach into the btree using fileCursor
	// and find it.
    } else {
	void *file=NULL;
	unsigned fileLen;
	
	    // Make certain we have a table to map extracted files.
	if( !extractedFiles) {
	    extractedFiles=[[HashTable alloc] initKeyDesc:"i" valueDesc:"%"];
	}
	
	    // Store the mapping for this index.
	[extractedFiles insertKey:(void *)fileId value:(void *)NXUniqueString( target)];
	
	    // Try to find the index in the table.
	if( [fileCursor setKey:(void *)(&fileId) andLength:sizeof( fileId)]) {
	    int fd;
	    const char *slash, *dot;
	    
		// Read the contents of the file.
	    fileLen=[fileCursor readValue:&file];
	    
		// Open the file and stuff the contents into it.
	    fd=open( target, O_WRONLY|O_CREAT, 0644);
	    if( fd!=-1) {
		if( write( fd, file, fileLen)<fileLen) {
		    perror( "uncompresshelp: Writing file");
		}
		if( close( fd)==-1) {
		    perror( "uncompresshelp: Closing file");
		}
	    } else {
		perror( "uncompresshelp: Opening file");
	    }
	    
		// Free the contents.
	    free( file);
	    
		// If the file is .rtf, uncompress it.
	    slash=rindex( target, '/');
	    slash=(slash ? slash+1 : target);
	    dot=rindex( slash, '.');
	    if( dot) {
		if( !strcmp( dot, ".rtf") || !strcmp( slash, ".index.store")) {
		    char targetZ[ MAXPATHLEN+1];
		    sprintf( targetZ, "%s.Z", target);
		    fprintf( stderr, "\tRenaming %s\n\t      to %s\n", target, targetZ);
		    rename( target, targetZ);
		    fprintf( stderr, "\tUncompressing %s\n", targetZ);
		    uncompress( targetZ);
		}
	    }
	}
    }
}
    // Create the directory, creating parent directories as needed.
int mkdirs( char *dir, int mode)
{
    if( access( dir, F_OK)) {
	int ret=0;
	char *slash=rindex( dir, '/');
	if( slash) {
	    *slash='\0';
	    ret=mkdirs( dir, mode);
	    *slash='/';
	}
	if( !ret) {
	    ret=mkdir( dir, mode);
	}
	return ret;
    }
    return 0;
}
    // *files is a file list in the format "filename key;[ filename
    // key;...]".  Parse out the first filename/key pair into
    // outputp/*indexp, and put the end position back into *files.
    // Return YES if a filename was found.  This routine is more
    // complex than needed because I was concerned that my only
    // knowledge of what's possible for filenames is empirical -
    // I know what's in some sample files, but I don't know whether
    // there are other possibilities (consider spaces in filenames,
    // for instance).  Better safe than sorry.
BOOL getNextFile( const char **files, char *outputp, unsigned *indexp)
{
    const char *s, *p;
    for( s=*files; *s; s++) {
	    // Get to a space followed by a digit.
	if( *s==' ' && isdigit( s[ 1])) {
		// Start accumulating the index.
	    unsigned i=s[ 1]-'0';
		// Store away the end of the name.
	    p=s;
		// Accumulate the rest of the index.
	    for( s+=2; isdigit( *s); s++) {
		i*=10;
		i+=s[ 0]-'0';
	    }
		// It _must_ end with ; to be valid.
	    if( *s==';') {
		    // Store away the file name.
		strncpy( outputp, *files, p-(*files));
		outputp[ p-(*files)]='\0';
		    // Store away the index found.
		*indexp=i;
		    // Return the new search position, skipping
		    // the extra space after interior elements of
		    // the list.
		*files=(s[ 1] ? s+2 : s+1);
		return YES;
	    }
	}
    }
    return NO;
}
void main( int argc, char **argv)
{
    const char *inputFile="Help.store";
    char outputDir[ MAXPATHLEN+1]="Help";
    unsigned outputDirLen;
    IXStoreFile *helpStore;
    IXBTree *dirTree, *fileTree;
    IXBTreeCursor *dirCursor, *fileCursor;

	// Decode command-line parameters.
    if( argc>1) {
	inputFile=argv[ 1];
	if( argc>2) {
	    strcpy( outputDir, argv[ 2]);
	} else {
	    const char *dot=rindex( inputFile, '.');
	    if( dot && !strcmp( dot, ".store")) {
		strncpy( outputDir, inputFile, dot-inputFile);
		outputDir[ dot-inputFile]='\0';
	    } else {
		fprintf( stderr, "uncompresshelp:\tUnable to generate default outputDir:\n\t\t%s doesn't end in .store.\n", inputFile);
		exit( 1);
	    }
	}
    }
    outputDirLen=strlen( outputDir);
    outputDir[ outputDirLen++]='/';
    fprintf( stderr, "Extracting %s into %s\n", inputFile, outputDir);
    
	// Open up the storefile.
    helpStore=[[IXStoreFile alloc] initFromFile:inputFile forWriting:NO];
    if( !helpStore) {
	fprintf( stderr, "Couldn't open %s\n", inputFile);
	exit( 1);
    }
    
	// Open up a tree to list our directories and files in
	// them.
    dirTree=[[IXBTree alloc] initFromBlock:1 inStore:helpStore];
    dirCursor=[[IXBTreeCursor alloc] initWithBTree:dirTree];
    
	// Open up a tree to map indices to files.
    fileTree=[[IXBTree alloc] initFromBlock:2 inStore:helpStore];
    fileCursor=[[IXBTreeCursor alloc] initWithBTree:fileTree];

	// Start from the first entry in the directory list.
    if( [dirCursor setFirst]) {
	do {
	    char *dir=NULL, *files=NULL;
	    const char *s;
	    unsigned dirLen, filesLen;
	    
		// Get the directory entry.
	    if( [dirCursor getKey:(void **)&dir andLength:&dirLen]) {
		unsigned i;
		const char *s;
		
		    // Append the entry to the target directory.
		strncpy( outputDir+outputDirLen, dir, dirLen);
		outputDir[ outputDirLen+dirLen-1]='\0';
		
		    // Read the list of files for that entry.
		filesLen=[dirCursor readValue:(void **)&files];
		
		    // Look for the first file in the list.
		s=files;
		if( getNextFile( &s, outputDir+outputDirLen+dirLen, &i)) {
			// If that's the end, check to see if the
			// directory entry's basename matches the
			// only entry in the directory.
		    if( !*s) {
			char *b=rindex( outputDir, '/');
			if( b && !strcmp( b+1, outputDir+outputDirLen+dirLen)) {
				// If they match, then extract directly
				// into the name specified by the
				// directory entry.
			    fprintf( stderr, "Extracting simple file %s\n", outputDir);
			    *b='\0';
			    mkdirs( outputDir, 0755);
			    *b='/';
			    extractFile( fileCursor, i, outputDir);
				// And go to the next entry.
			    continue;
			}
		    }
		    
			// Make certain the directory(s) for the
			// entry exist.
		    mkdirs( outputDir, 0755);
		    fprintf( stderr, "Extracting compound file %s\n", outputDir);
		    
			// Put a slash between the parent dir and
			// the filenames within it.
		    outputDir[ outputDirLen+dirLen-1]='/';
		    
			// Extract each of the contained files.
		    do {
			fprintf( stderr, "    Extracting %s\n", outputDir);
			extractFile( fileCursor, i, outputDir);
		    } while( getNextFile( &s, outputDir+outputDirLen+dirLen, &i));
		}
		free( files);
	    }
	} while( [dirCursor setNext]);
    }
}
